<!-- Histogram Maker

This is a histogram maker for Cognos BI reports built with Report
Studio.  A histogram visually represents the distribution of values
(how frequently different values occur) for a single variable.  For
example, a histogram could display the distribution of scores on a
test.

Instructions:

In Cognos Report Studio, create a list with two columns.

The first column contains values of the variable in question. It
need only list values for which the frequency > 0. The column title
will become the label of the histogram's horizontal axis.

The second column contains frequencies (counts) for each value in the
first column. Its title will label the histogram's verical axis.

Optional additional columns, after the first two:

These columns can set the range and bin width of the histogram. Each
of these contains the same value in each row, which may be set by a
report expression (good for constant, preset values) or a calculated
data item (good if you want to use summary functions like minimum and
maximum.

   column title   effect
   min            minimum value for histogram range (default 0)
   max            maximum value for histogram range (default largest
                  value in first column)
   binwidth       width of each frequency bin in the histogram
                  (default 1)

   Note: If the histogram range is from 200 to 280, the histogram will
   display a colored background identifying the four performance level
   ranges for MCAS scaled scores: warning/failing; needs improvement;
   proficient; and advanced or beyond proficient.

Set the list's Name property to 'histogram' plus optional identifying
text plus 'data'. The simplest possible name is histogramdata. An
example of an expanded name is histogramScaledScoredata. If you want
to preserve the list in the report output, add 'keep' to the end of
the name. histogramScaledScoredatakeep, for example. If you don't add
keep to the name, the histogram will appear in the report and the list
will be hidden.

Place a block (from the insertable objects toolkit) in your report,
immediately before the list. The histogram will appear in the block.
Set the block's Name property to the same value as the list but
without 'data' or 'keep' at the end. Corresponding to the examples
above, you could name the block histogram or histogramScaledScore. To
control the size and color of the histogram, set the size and
foreground color properties of the block.

Insert an HTML item near the end of your report, after all the
histogram lists.

Set the HTML property of the HTML item to the contents of this file,
using copy and paste.

To control the histogram, add additional columns to the table: min, max, and/or binwidth. They should have the
same value in each row (may be a report expression or a summary data expression).

-->
<script type="text/javascript">

//console.log('CEDU histogram for Cognos.');

// Place everything into a single function except the block and list lists.
var ceduHist = { blocks: [], blocksizes: [], lists: [] };
function ceduHistogram() {

   // Set up a frequency table, where f is a list of [value, frequency] pairs.
   // Titles and data will come from a report list.
   var ftable = {valueTitle: '', frequencyTitle: '', f: []};

   // Store variable range and histogram bin width.
   var min, max, binwidth;

   // Set up an array of histogram bins.
   var bins = [];

   // Provide utility functions for bin calculations.
   function binLo(bin) { return min + bin * binwidth };
   function binHi(bin) { return min + (bin + 1) * binwidth };
   function mkbins() {
      // Create an array of frequency bins covering (min, max) with binwidth spans.
      // (Create the frequency table before calling this.)
      bins = new Array(Math.ceil((max - min) / binwidth));
      for (var i = 0; i < bins.length; i++) {
         bins[i] = 0;
      }
      // Add each frequency from the current frequency table to a corresponding bin.
      // Bins include their lower limit but not their upper limit,
      // excepting the last bin includes all higher values, and the first bin includes all lower values.
      for (var i = 0; i < ftable.f.length; i++) {
         bins[Math.max(0, Math.min(bins.length - 1, Math.floor((ftable.f[i][0] - min) / binwidth)))] += ftable.f[i][1];
      }
   };
   function maxFreq() {
      // Return the maximum frequency from the histogram bins.
      var max = 0;
      for (var i = 0; i < bins.length; i++)
         if (bins[i] > max) max = bins[i];
      return max;
   }
   function setMaxValue() {
      // Find and record the maximum data value if not preset.
      if (max === null) {
         for (var i = 0; i < ftable.f.length; i++)
            if (max === null || ftable.f[i][0] > max) max = ftable.f[i][0];
         return max;
      }
   }

   // Create URLs for the Google Chart API.
   var url;
   function simpleEncode(values, max) {
      // Translate values to characters in "simple encoding" format for Google Chart Tools.
      // Return a character string to be used as a value of the chd parameter in a Google Chart URL.
      var encoding = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
      var str = ['s:'];
      for (var i = 0; i < values.length; i++) {
         var v = values[i];
         if (!isNaN(v) && v >= 0)
            str.push(encoding.charAt(Math.round((encoding.length-1) * v / max)));
         else
            str.push('_');  // missing value
      }
      return str.join('');
   }
   function mkurl(i) {
      // Create the frequency table and call mkbins first, of course.
      // The URL has a number of parameters, which we will attach as properties to par
      // before finally stringifying all of them.
      // For starters, chart type = vertical grouped bar.
      var par = {cht: 'bvg'};
      // bars automatically sized, no space between bars or groups
      par.chbh = 'a,0,0';
      // chart size, optionally controlled by block element
      var w = ceduHist.blocksizes[i].w ? ceduHist.blocksizes[i].w : 400;
      var h = ceduHist.blocksizes[i].h ? ceduHist.blocksizes[i].h : 225;
      par.chs = w + 'x' + h;
      // encoded data
      par.chd = simpleEncode(bins, maxFreq());
      // color, optionally controlled by block element
      par.chco = ceduHist.blocksizes[i].c ? ceduHist.blocksizes[i].c : '4D89F9';  // default bright blue
      // background
      par.chf = 'bgfE8E8E8';
      // x axis
      par.chxt = 'x';
      // x axis range
      par.chxr = '0,' + min + ',' + max;
      // Label the lower and upper *edges* of the bars. There will be one more label than bars.
      // Position labels in terms of the x scale.
      par.chxp = '0,';
      for (var i = 0; i < bins.length; i++)
         par.chxp += binLo(i) + ',';  // lower edges
      par.chxp += binHi(bins.length - 1);  // upper edge of last bin
      // label format - dark gray, computed font size, centered
      var fontSizePixels = 10;
      if (max > 100) fontSizePixels = 8;
      par.chxs = '0,404040,' + fontSizePixels + ',0';
      // no tick marks
      par.chxtc = '0,0';
      // y axis
      par.chxt += ',y';
      // axis range
      par.chxr += '|1,0,' + maxFreq();
      // Assemble our URL parameter string.
      var parString = '';
      var p;  // iterator
      for (p in par) {
         if (parString) parString += '&';
         parString += p + '=' + par[p];
      }
      // Create and store the URL.
      url = 'http://chart.apis.google.com/chart?' + parString;
   }

   // Add MCAS performance level shading to a scaled score distribution.
   function shadePerformanceLevels() {
      url += '&chm=R,FED2D2,0,0,0.25|R,FFEEEE,0,0.25,0.5|R,C2C8EA,0,0.5,0.75|R,94C9FE,0,0.75,1';
   }

   // Set histogram data storage to initial, default condition prior to processing a source list.
   function defaults() {
      min = 0, max = null, binwidth = 1;
      ftable = {valueTitle: '', frequencyTitle: '', f: []};
      bins = [];
      url = '';
   }

   // Locate Cognos elements by their Name property, expressed in HTML as an 'lid' attribute.
   function findElements() {
      // Our histograms go into blocks (div.block) with Cognos names (lid values) starting with 'histogram'.
      blocks = $('div.block[lid^="histogram"]')
      var iBlock, block;
      for (iBlock = 0; iBlock < blocks.length; iBlock++) {
         block = blocks[iBlock];
         var i = ceduHist.blocks.push(block) - 1;  // use index for blocksizes
         // Record the block size and foreground color, if specified.
         ceduHist.blocksizes[i] = {w: null, h: null, c: null};
         if (block.style) {
            if (block.style.width) {
               var w = parseInt(block.style.width);
               if (!isNaN(w)) ceduHist.blocksizes[i].w = w;
            }
            if (block.style.height) {
               var h = parseInt(block.style.height);
               if (!isNaN(h)) ceduHist.blocksizes[i].h = h;
            }
            if (block.style.color) {
               var rgbStr = block.style.color;
               // Hoping it always looks like rgb(0, 128, 0)...
               var rgbExp = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/;
               var rgbResult = rgbExp.exec(rgbStr);
               // Google Chart color parameter is six hex digits, RRGGBB.
               function hex(val) { var s = Number(val).toString(16).toUpperCase(); return s.length == 1 ? '0' + s : s; }
               if (rgbResult) {
                  ceduHist.blocksizes[i].c = hex(rgbResult[1]) + hex(rgbResult[2]) + hex(rgbResult[3]);
               }
            }
         }
         //console.log('found block ' + ceduHist.blocks.length + ', ' + block.getAttribute('lid') + ', ' + ceduHist.blocksizes[i].w + 'x' + ceduHist.blocksizes[i].h + ', ' + ceduHist.blocksizes[i].c);
         // Find the associated list (an HTML table with the same Cognos name + 'data' + possibly more).
         var listNameStarts = block.getAttribute('lid') + 'data';
         //console.log('looking for ' + listNameStarts);
         var foundLists = $('. ~ siblings.ls[lid^="' + listNameStarts + '"]', block);
         var list = document.evaluate( 
            'following-sibling::table[@class="ls" and starts-with(@lid, "' + listNameStarts + '")]', 
            block, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null );
         if (list.singleNodeValue) {
            list = list.singleNodeValue;
         } else {
            alert('I cannot find a list whose name starts with ' + listNameStarts + '! The histogram maker is stopping.');
            throw('no ' + listNameStarts + ' list found');
         }
         ceduHist.lists.push(list);
         //console.log('found list ' + list.getAttribute('lid'));
      }
   }

   // Extract values and frequency data from a list.
   function getFreq(list, ftable) {
      // Capture values by row.
      var rowIter = document.evaluate( 'tbody/tr', list, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
      var tr;
      while (tr = rowIter.iterateNext()) {
         var cellIter = document.evaluate( 'td', tr, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
         var td;
         while (td = cellIter.iterateNext()) {
            switch (td.getAttribute('type')) {  // Cognos uses a 'type' attribute.
              case 'columnTitle':
                  switch (td.getAttribute('cid')) {
                     case '0':  // title of first column = value title
                        ftable.valueTitle = td.textContent;
                        break;
                     case '1':  // title of first column = frequency title
                        ftable.frequencyTitle = td.textContent;
                        break;
                     default:  // check for optional min, max, binwidth columns
                        switch (td.textContent) {
                           // Here we'll set min/max/binwidth to something like 'column3'.
                           // The first data row will replace that with an actual min/max/binwidth value.
                           case 'min':
                              min = 'column' + td.getAttribute('cid');
                              break;
                           case 'max':
                              max = 'column' + td.getAttribute('cid');
                              break;
                           case 'binwidth':
                              binwidth = 'column' + td.getAttribute('cid');
                              break;
                        }
                        break;
                  }
                  break;
               case 'datavalue':
                  switch (td.getAttribute('cid')) {
                     case '0':  // Here is a value. Create a new [value, frequency] pair sans frequency.
                        ftable.f.push([Number(td.textContent)]);
                        break;
                     case '1':  // Here is the frequency. Complete the [value, frequency] pair.
                        ftable.f[ftable.f.length - 1].push(Number(td.textContent));
                        break;
                     default:  // Check for min, max, binwidth columns tagged by title row processing.
                        if (min == 'column' + td.getAttribute('cid'))
                           min = Number(td.textContent);
                        else if (max == 'column' + td.getAttribute('cid'))
                           max = Number(td.textContent);
                        else if (binwidth == 'column' + td.getAttribute('cid'))
                           binwidth = Number(td.textContent);
                        break;
                  }
                  break;
            }
         }
      }
      setMaxValue();
   }

   // Insert a histogram into the document.
   function insertHistogram(url, block) {
      // Remove any existing image (histogram).
      var imgs = block.getElementsByTagName('IMG');
      if (imgs.item(0)) block.removeChild(imgs.item(0));
      // Create a new image for the histogram.
      var img = document.createElement('IMG');
      img.src = url;
      block.appendChild(img);
   }

   // Remove a source data list from the document unless its name has 'keep' in it.
   function removeList(i) {
      var list = ceduHist.lists[i];
      if (list.getAttribute('lid').indexOf('keep') == -1)
         list.parentNode.removeChild(list);
   }

   // Process all lists.
   function makeAll() {
      findElements();
      for (var i in ceduHist.lists) {
         defaults();
         getFreq(ceduHist.lists[i], ftable);
         mkbins();
         mkurl(i);
         // Shade performance levels if the range is 200 to 280.
         if (min == 200 && max == 280) shadePerformanceLevels();
         insertHistogram(url, ceduHist.blocks[i]);
         removeList(i);
      }
   }

   makeAll();
}

ceduHistogram();

//console.log('CEDU finished.');

</script>
